/*

Miranda IM: the free IM client for Microsoft* Windows*

Copyright 2000-2007 Miranda ICQ/IM project, 
all portions of this codebase are copyrighted to the people 
listed in contributors.txt.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
aLONG with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

/**
 * System Includes:
 **/
#include "commonheaders.h"
#include "m_skin.h"
#include "m_clui.h"
#include "m_message.h"
#include "m_contacts.h"

#include "ExtraServices.h"
#include "Reminder.h"
#include "dlgAnniversaryList.h"

INT GetContactInfo(WPARAM wParam, LPARAM lParam) ;

namespace NReminder {

#define HOTKEYCLASS _T("ReminderHotkeys")

typedef struct TReminderOptions {
	WORD wDaysEarlier;
	BYTE bPopups;
	BYTE bCListExtraIcon;
	BYTE bFlashCList;
	BYTE bCheckVisibleOnly;
	BYTE RemindState;
	RemindEvent nextAnniv;
} REMINDEROPTIONS;


static HANDLE	ghCListIA					= NULL;
static HANDLE	ghCListIR					= NULL;
static HANDLE	ghSettingsChanged			= NULL;

static UINT_PTR	ghRemindTimer				= 0;
static UINT_PTR	ghRemindDateChangeTimer		= 0;

HWND			ghWndHotkey					= NULL;
HANDLE			ghCListAnnivIcons[11];
HANDLE			ghCListBirthdayIcons[11];

static REMINDEROPTIONS	gRemindOpts;


static VOID UpdateTimer(BOOLEAN bStartup);

/***********************************************************************************************************
 * window procedures
 ***********************************************************************************************************/

/**
 * name:	Hotkey_WndProc
 * desc:	window procedure, that handles messages from a displayed popup
 * param:	hWnd	- same as ghWndHotkey
 *			uMsg	- message to handle
 *			wParam	- message specific parameter
 *			lParam	- message specific parameter
 **/
static LRESULT CALLBACK Hotkey_WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg) {
		case WM_NCCREATE:
		case WM_CREATE:
			return TRUE;
		case HM_OPENMSG:
			CallService(MS_MSG_SENDMESSAGE, wParam, lParam);
			break;
		case WM_TIMECHANGE:
			UpdateTimer(FALSE);
			break;
	}
	return FALSE;
}

/**
 * name:   PopupWindowProc
 * desc:   message procedure for popup messages
 * param:   hWnd   - handle to the popupwindow
 *         uMsg   - message to handle
 *         wParam   - message specific parameter
 *         lParam   - message specific parameter
 *
 **/
static LRESULT CALLBACK Popup_WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 
{
   switch(uMsg) {
      case WM_COMMAND: {
         if(HIWORD(wParam) == STN_CLICKED) {
            HANDLE hContact = PUGetContact(hWnd);
            if(hContact) {
               PostMessage(ghWndHotkey, HM_OPENMSG, (WPARAM)hContact, NULL);
            }
            PUDeletePopUp(hWnd);
            return TRUE;
         }
         break;
      }
      case WM_CONTEXTMENU:
         PUDeletePopUp(hWnd);
         return TRUE;
      case UM_FREEPLUGINDATA:
         return FALSE;
   }
   return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

/***********************************************************************************************************
 * notification functions
 ***********************************************************************************************************/

static HANDLE AddCListExtraIcon(const BYTE eventType, const WORD bDaysLeft)
{
	const LPCSTR pszIco[2][11] = {
		{
			ICO_RMD_DTB0, ICO_RMD_DTB1, ICO_RMD_DTB2, ICO_RMD_DTB3, ICO_RMD_DTB4, ICO_RMD_DTB5,
			ICO_RMD_DTB6, ICO_RMD_DTB7, ICO_RMD_DTB8, ICO_RMD_DTB9, ICO_RMD_DTBX 
		},
		{
			ICO_RMD_DTA0, ICO_RMD_DTA1, ICO_RMD_DTA2, ICO_RMD_DTA3, ICO_RMD_DTA4, ICO_RMD_DTA5,
			ICO_RMD_DTA6, ICO_RMD_DTA7, ICO_RMD_DTA8, ICO_RMD_DTA9, ICO_RMD_DTAX 
		}
	};
	HANDLE tmp = (HANDLE)CallService(MS_CLIST_EXTRA_ADD_ICON, (WPARAM) NIcoLib::GetIcon(pszIco[eventType][bDaysLeft]), 0);
	return ((tmp != (HANDLE)CALLSERVICE_NOTFOUND) ? tmp : INVALID_HANDLE_VALUE);
}

static HANDLE GetCListExtraIcon(RemindEvent *nextAnniv)
{
	if(gRemindOpts.bCListExtraIcon) {
		WORD wIndex = nextAnniv->_wDaysLeft;

		switch(nextAnniv->_bType) {
			case EVENT_BIRTHDAY:
				if(wIndex >= SIZEOF(ghCListBirthdayIcons))
					wIndex = SIZEOF(ghCListBirthdayIcons) - 1;
				// add the icon to clists imagelist if required
				if(ghCListBirthdayIcons[wIndex] == INVALID_HANDLE_VALUE)
					ghCListBirthdayIcons[wIndex] = AddCListExtraIcon(0, wIndex);
				return ghCListBirthdayIcons[wIndex];

			case EVENT_ANNIVERSARY:
				if(wIndex >= SIZEOF(ghCListAnnivIcons))
					wIndex = SIZEOF(ghCListAnnivIcons) - 1;
				// add the icon to clists imagelist if required
				if(ghCListAnnivIcons[wIndex] == INVALID_HANDLE_VALUE)
					ghCListAnnivIcons[wIndex] = AddCListExtraIcon(1, wIndex);
				return ghCListAnnivIcons[wIndex];
		}
	}
	return INVALID_HANDLE_VALUE;
}

static VOID NotifyWithExtraIcon(HANDLE hContact, RemindEvent *nextAnniv)
{
	IconExtraColumn iec;

	if(myGlobals.HaveCListExtraIcons && (iec.ColumnType = gRemindOpts.bCListExtraIcon)) {
		iec.cbSize = sizeof(IconExtraColumn);
		iec.hImage = GetCListExtraIcon(nextAnniv);
		CallService(MS_CLIST_EXTRA_SET_ICON, (WPARAM)hContact, (LPARAM)&iec);
	}
}

static VOID NotifyClearIcon(HANDLE hContact)
{
	IconExtraColumn iec;

	if(myGlobals.HaveCListExtraIcons && (iec.ColumnType = gRemindOpts.bCListExtraIcon)) {
		iec.cbSize = sizeof(IconExtraColumn);
		iec.hImage = INVALID_HANDLE_VALUE;
		CallService(MS_CLIST_EXTRA_SET_ICON, (WPARAM)hContact, (LPARAM)&iec);
	}
}

/**
 * name:	NotifyWithPopup
 * desc:	Displays a popup
 * param:	ct			- information about the contact to show the display for
 *			eventType	- indicates which popup settings to apply
 *			szMsg		- message to display
 * return:	nothing
 **/
static INT NotifyWithPopup(HANDLE hContact, EventType eventType, LPCTSTR pszDesc, LPCTSTR pszMsg)
{
	POPUPDATAT ppd;

	// popups disabled?
	if(!gRemindOpts.bPopups)
		return 1;

	ZeroMemory(&ppd, sizeof(POPUPDATAT));
		
	if(hContact) {
		ppd.lchContact = hContact;
#ifdef _UNICODE
		mir_snprintfW(ppd.lpwzContactName, 
			SIZEOF(ppd.lpwzContactName), 
			L"%s - %s", TranslateW(pszDesc),
			DBGetContactDisplayName(hContact));
#else
		mir_snprintfA(ppd.lpzContactName, 
			SIZEOF(ppd.lpzContactName), 
			"%s - %s", Translate(pszDesc),
			DBGetContactDisplayName(hContact));
#endif
	}
	else {
#ifdef _UNICODE
		mir_wcsncpy(ppd.lpwzContactName, TranslateT("Reminder"), SIZEOF(ppd.lpwzContactName));
#else
		mir_strncpy(ppd.lpzContactName, Translate("Reminder"), SIZEOF(ppd.lpzContactName));
#endif
	}
#ifdef _UNICODE
	mir_wcsncpy(ppd.lpwzText, pszMsg, MAX_SECONDLINE);
#else
	mir_strncpy(ppd.lpzText, pszMsg, MAX_SECONDLINE);
#endif

	ppd.PluginWindowProc = (WNDPROC)Popup_WindowProc;
	ppd.iSeconds = (INT)DBGetByte(SET_POPUP_DELAY, 0);
	switch(eventType) {
		case EVENT_BIRTHDAY:
			switch(DBGetByte(SET_POPUP_BIRTHDAY_COLORTYPE, POPUP_COLOR_CUSTOM)) {
				case POPUP_COLOR_WINDOWS:
					ppd.colorBack = GetSysColor(COLOR_BTNFACE);
					ppd.colorText = GetSysColor(COLOR_WINDOWTEXT);
					break;
				case POPUP_COLOR_CUSTOM:
					ppd.colorBack = DBGetSettingDword(NULL, MODNAME, SET_POPUP_BIRTHDAY_COLOR_BACK, RGB(192,180,30));
					ppd.colorText = DBGetSettingDword(NULL, MODNAME, SET_POPUP_BIRTHDAY_COLOR_TEXT, 0);
					break;
			}
			ppd.lchIcon = NIcoLib::GetIcon(ICO_COMMON_BIRTHDAY, IDI_BIRTHDAY);
			break;
		default:
			switch(DBGetByte(SET_POPUP_ANNIVERSARY_COLORTYPE, POPUP_COLOR_CUSTOM)) {
				case POPUP_COLOR_WINDOWS:
					ppd.colorBack = GetSysColor(COLOR_BTNFACE);
					ppd.colorText = GetSysColor(COLOR_WINDOWTEXT);
					break;
				case POPUP_COLOR_CUSTOM:
					ppd.colorBack = DBGetSettingDword(NULL, MODNAME, SET_POPUP_ANNIVERSARY_COLOR_BACK, RGB(90, 190, 130));
					ppd.colorText = DBGetSettingDword(NULL, MODNAME, SET_POPUP_ANNIVERSARY_COLOR_TEXT, 0);
					break;
			}
			ppd.lchIcon = NIcoLib::GetIcon(ICO_COMMON_ANNIVERSARY, IDI_ANNIVERSARY);
			break;
	}
	return PUAddPopUpT(&ppd);
}

static VOID NotifyFlashCListIcon(HANDLE hContact, RemindEvent *nextAnniv)
{
	if(gRemindOpts.bFlashCList && nextAnniv->_wDaysLeft == 0) {
		CLISTEVENT cle;
		TCHAR szMsg[MAX_PATH];

		switch(nextAnniv->_bType) {
			case EVENT_BIRTHDAY:
				mir_sntprintf(szMsg, SIZEOF(szMsg), 
					TranslateT("%s has %s today."), 
					DBGetContactDisplayName(hContact), 
					TranslateT("Birthday"));
				cle.hIcon = NIcoLib::GetIcon(ICO_COMMON_BIRTHDAY);
				break;

			case EVENT_ANNIVERSARY:
				mir_sntprintf(szMsg, SIZEOF(szMsg),
					TranslateT("%s has %s today."),
					DBGetContactDisplayName(hContact), 
					TranslateT("an anniversary"));
				cle.hIcon = NIcoLib::GetIcon(ICO_COMMON_ANNIVERSARY);
				break;
		}

		cle.cbSize = sizeof(cle);
		cle.flags = CLEF_URGENT|CLEF_TCHAR;
		cle.hContact = hContact;
		cle.hDbEvent = NULL;
		cle.pszService = NULL;
		cle.ptszTooltip = szMsg;
		CallService(MS_CLIST_ADDEVENT, 0, (LPARAM)&cle);
	}
}

/**
 * name:	NotifyWithSound
 * desc:	Play a sound for the nearest upcomming anniversary
 * param:	nothing
 * return:	nothing
 **/
static BYTE NotifyWithSound(RemindEvent *nextAnniv)
{
	if(nextAnniv->_wDaysLeft <= min(DBGetByte(SET_REMIND_SOUNDOFFSET, DEFVAL_REMIND_SOUNDOFFSET), gRemindOpts.wDaysEarlier)) {
		switch(nextAnniv->_bType) {
			case EVENT_BIRTHDAY:
				SkinPlaySound(nextAnniv->_wDaysLeft == 0 ? SOUND_BIRTHDAY_TODAY : SOUND_BIRTHDAY_SOON);
				return 0;

			case EVENT_ANNIVERSARY:
				SkinPlaySound(SOUND_ANNIVERSARY);
				return 0;
		}
	}
	return 1;
}

/***********************************************************************************************************
 * "check for anniversary" functions
 ***********************************************************************************************************/

static __forceinline LPCTSTR ContactGender(HANDLE hContact)
{
	switch(NServices::NGender::DBGetContactGender(hContact)) {
		case 'M': return TranslateT("He");
		case 'F': return TranslateT("She");
	}
	return TranslateT("He/She");
}

static BOOLEAN CheckAnniversaries(HANDLE hContact, MTime *pNow, RemindEvent *nextAnniv, BOOLEAN bNotify)
{
	INT numAnniversaries = 0;
	INT Diff;
	MAnnivDate mta;
	INT i;
	TCHAR szAnniv[MAX_PATH];
	tstring strMsg;
	BOOLEAN bOverflow = FALSE;
	WORD wDaysEarlier;

	if(gRemindOpts.RemindState != REMIND_ANNIV && gRemindOpts.RemindState != REMIND_ALL)
		return FALSE;	
		
	for(i = 0; i < ANID_LAST && !mta.DBGetAnniversaryDate(hContact, i); i++) {
		mta.DBGetReminderOpts(hContact);
		// check contact specific reminder options
		switch(mta.RemindOption()) {
			case BST_UNCHECKED:
				return FALSE;
			case BST_CHECKED:
				wDaysEarlier = mta.RemindOffset();
				if(wDaysEarlier != (WORD)-1)
					break;
			case BST_INDETERMINATE:
			default:
				wDaysEarlier = gRemindOpts.wDaysEarlier;
				break;
		}
		Diff = mta.CompareDays(*pNow);
		if(Diff >= 0 && Diff <= wDaysEarlier) {

			if(nextAnniv->_wDaysLeft > Diff) {
				nextAnniv->_wDaysLeft = Diff;
				nextAnniv->_bType = EVENT_ANNIVERSARY;
			}
			numAnniversaries++;
			
			// create displayed text for popup
			if(bNotify && !bOverflow && strMsg.length() < MAX_SECONDLINE) {
				// first anniversary found
				if(numAnniversaries == 1) {
					mir_sntprintf(szAnniv, MAX_PATH, TranslateT("%s has the following anniversaries:"), ContactGender(hContact));
					strMsg.append(szAnniv);
				}
				switch(Diff) {
					case 0: 
						mir_sntprintf(szAnniv, MAX_PATH, TranslateT("%d. %s today"), mta.Age(), mta.Description());
						break;
					case 1: 
						mir_sntprintf(szAnniv, MAX_PATH, TranslateT("%d. %s tomorrow"), mta.Age() + 1, mta.Description());
						break;
					default:
						mir_sntprintf(szAnniv, MAX_PATH, TranslateT("%d. %s after %d days"), mta.Age() + 1, mta.Description(), Diff);
						break;
				}
				if(_tcslen(szAnniv) >= MAX_SECONDLINE - strMsg.length()) {
					strMsg.append(_T("\n..."));
					bOverflow = TRUE;
				}
				else {
					strMsg.append( _T("\n- "));
					strMsg.append(szAnniv);
				}
			}
		}
	}
	// show one popup for all anniversaries
	if(numAnniversaries != 0 && bNotify) {
		NotifyWithPopup(hContact, EVENT_ANNIVERSARY, _T("Anniversaries"), strMsg.c_str());
	}
	return numAnniversaries != 0;
}

static BOOLEAN CheckBirthday(HANDLE hContact, MTime *pNow, RemindEvent *nextAnniv, BOOLEAN bNotify)
{
	INT Diff;
	MAnnivDate mtb;
	TCHAR szMsg[MAXDATASIZE];
	WORD cchMsg = 0;
	WORD wDaysEarlier;

	if(gRemindOpts.RemindState != REMIND_BIRTH && gRemindOpts.RemindState != REMIND_ALL)
		return FALSE;	
	if(mtb.DBGetBirthDate(hContact))
		return FALSE;
	mtb.DBGetReminderOpts(hContact);
	
	// make backup of each protocol based birthday
	//mtb.NewProtocolBirthday(hContact, NULL, 0, &_pReminder->_nLastAnwer);

	// check contact specific reminder options
	switch(mtb.RemindOption()) {
		case BST_UNCHECKED:
			return FALSE;
		case BST_CHECKED:
				wDaysEarlier = mtb.RemindOffset();
				if(wDaysEarlier != (WORD)-1)
					break;
		case BST_INDETERMINATE:
		default:
			wDaysEarlier = gRemindOpts.wDaysEarlier;
			break;
	}
	
	Diff = mtb.CompareDays(*pNow);
	if(Diff < 0 || Diff > wDaysEarlier)
		return FALSE;

	if(nextAnniv->_wDaysLeft > Diff) {
		nextAnniv->_wDaysLeft = Diff;
		nextAnniv->_bType = EVENT_BIRTHDAY;
	}

	if(bNotify) {
		switch(Diff) {
			case 0:
				cchMsg = mir_sntprintf(szMsg, SIZEOF(szMsg),
					TranslateT("%s has %s today."),
					DBGetContactDisplayName(hContact), mtb.Description());
				break;
			case 1:
				cchMsg = mir_sntprintf(szMsg, SIZEOF(szMsg),
					TranslateT("%s has %s tomorrow."),
					DBGetContactDisplayName(hContact), mtb.Description());
				break;
			default:
				cchMsg = mir_sntprintf(szMsg, SIZEOF(szMsg),
					TranslateT("%s has %s after %d days."),
					DBGetContactDisplayName(hContact), mtb.Description(), Diff);
				break;
		}
		mir_sntprintf(szMsg+cchMsg, SIZEOF(szMsg)-cchMsg,
			TranslateT("\n%s becomes %d years old."),
			ContactGender(hContact), mtb.Age(pNow) + (Diff > 0));
		NotifyWithPopup(hContact, EVENT_BIRTHDAY, mtb.Description(), szMsg);
	}
	return TRUE;
}


/**
 * name:	CheckContact
 * class:	Reminder
 * desc:	check one contact only. It is used for clist extra icon rebuild notification handler mainly
 * param:	nothing
 * return:	nothing
 **/
static VOID CheckContact(HANDLE hContact, MTime *pNow, RemindEvent *nextAnniv, BOOLEAN bNotify)
{
	// ignore meta subcontacts here as their birthday information are collected explicitly
	if(
		hContact && (
		  !gRemindOpts.bCheckVisibleOnly || 
		  !DBGetSettingByte(hContact, "CList", "Hidden", FALSE)
		) && (
		  !myGlobals.HaveMetaContacts ||
		  !DBGetSettingByte(hContact, META_PROTO, "IsSubcontact", 0) ||
		  !DBGetByte(SET_META_SCAN, TRUE)
		)
	  )
	{
		RemindEvent ca = { EVENT_NONE, 0xFFFF };

		if(CheckBirthday(hContact, pNow, &ca, bNotify) || CheckAnniversaries(hContact, pNow, &ca, bNotify)) {

			if(nextAnniv->_wDaysLeft > ca._wDaysLeft) {
				nextAnniv->_bType = ca._bType;
				nextAnniv->_wDaysLeft = ca._wDaysLeft;
			}

			// clist should show the icon for the nearest anniversary
			NotifyWithExtraIcon(hContact, &ca);
			
			// flash clist icon
			if(bNotify) NotifyFlashCListIcon(hContact, &ca);
		}
		else
			NotifyClearIcon(hContact);
	}
}

VOID CheckAll(const ENotify notify)
{
	if(gRemindOpts.RemindState != REMIND_OFF) {
		HANDLE hContact;
		RemindEvent nextAnniv;
		MTime now;

		now.GetLocalTime();
		nextAnniv._bType = EVENT_NONE;
		nextAnniv._wDaysLeft = 0xFFFF;

		//walk through all the contacts stored in the DB
		for(hContact = (HANDLE)CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
			hContact != NULL;
			hContact = (HANDLE)CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM)hContact, 0))
		{
			CheckContact(hContact, &now, &nextAnniv, notify != NOTIFY_CLIST);
		}
		if(notify != NOTIFY_CLIST) {
			// play sound for the next anniversary
			NotifyWithSound(&nextAnniv);

			// popup anniversary list
			if(DBGetByte(SET_ANNIVLIST_POPUP, FALSE)) {
				NAnniversaryList::ShowDialog(0, 0);
			}

			if(nextAnniv._wDaysLeft > gRemindOpts.wDaysEarlier && notify == NOTIFY_NOANNIV)
				NotifyWithPopup(NULL, EVENT_NONE, NULL, TranslateT("No anniversaries to remind of"));
		}
		UpdateTimer(FALSE);
	}
}

/***********************************************************************************************************
 * Event Handler functions
 ***********************************************************************************************************/

/**
 * name:	OnCListRebuildIcons
 * desc:	Notification handler to tell reminder to reload required icons.
 *			It is because reminder only loads icons to clist, which are really required at the moment.
 *			This should help to save a bit memory.
 * param:	pszIcon	- name of the icon in the local icon library
 * return:	handle to the icon in the imagelist of clist
 **/
static INT OnCListRebuildIcons(WPARAM wParam, LPARAM lParam)
{
	UINT i;

	for(i = 0; i < SIZEOF(ghCListAnnivIcons); i++)
		ghCListAnnivIcons[i] = INVALID_HANDLE_VALUE;
	for(i = 0; i < SIZEOF(ghCListBirthdayIcons); i++)
		ghCListBirthdayIcons[i] = INVALID_HANDLE_VALUE;
	return 0;
}

/**
 * name:	OnCListApplyIcons
 * desc:	notification handler for clist extra icons to be applied for a contact
 * param:	wParam	- handle to the contact whose extra icon is to apply
 *			lParam	- not used
 **/
INT OnCListApplyIcons(WPARAM wParam, LPARAM lParam)
{
	if(gRemindOpts.RemindState != REMIND_OFF) {
		RemindEvent nextAnniv;
		MTime now;

		now.GetLocalTime();
		nextAnniv._bType = EVENT_NONE;
		nextAnniv._wDaysLeft = 0xFFFF;
		
		CheckContact((HANDLE)wParam, &now, &nextAnniv, FALSE);
	}
	return 0;
}

/**
 * name:	ContactSettingChanged
 * desc:	Notification handler for changed contact settings
 * param:	wParam	-	(HANDLE)hContact
 *			lParam	-	(DBCONTACTWRITESETTING*)pdbcws
 * return:	0 if setting was handled or 1 if not
 **/
static INT OnContactSettingChanged(WPARAM wParam, LPARAM lParam)
{
	DBCONTACTWRITESETTING* pdbcws = (DBCONTACTWRITESETTING*)lParam;
	
	if(ghCListIA && (*pdbcws->szSetting == 'B' || *pdbcws->szSetting == 'A')) {

		if(
		    !_strnicmp(pdbcws->szSetting, "Birth", 5) || 
		    !_strnicmp(pdbcws->szSetting, "Anniv", 5) ||
		    !_strnicmp(pdbcws->szSetting, "DOB", 3)
		  )
		{
			OnCListApplyIcons(wParam, 0);
		}
	}
	return 0;
}

/**
 * name:	OnTopToolBarModuleLoaded
 * desc:	adds a button to the toptoolbar for checking birthdays
 * param:	none
 * return:	0
 **/
VOID OnTopToolBarModuleLoaded()
{
	TTBButton ttb;
	HBITMAP hBmp = LoadBitmap(ghInst, MAKEINTRESOURCE(IDB_CHECK_ANNIV));

	ZeroMemory(&ttb, sizeof(TTBButton));
	ttb.cbSize = sizeof(TTBButton);
	ttb.dwFlags = TTBBF_VISIBLE | TTBBF_SHOWTOOLTIP;
	ttb.pszServiceDown = MS_USERINFO_REMINDER_CHECK;
	ttb.hbBitmapUp = ttb.hbBitmapDown = hBmp;
	ttb.name = Translate("Check birthdays");
	CallService(MS_TTB_ADDBUTTON, (WPARAM) &ttb, 0);
}


/***********************************************************************************************************
 * services
 ***********************************************************************************************************/

/**
 * name:	CheckService
 * desc:	service function for MS_USERINFO_REMINDER_CHECK
 * param:	wParam	-	not used
 *			lParam	-	not used
 * return:	0
 **/
static INT CheckService(WPARAM wParam, LPARAM lParam)
{
	CheckAll(NOTIFY_NOANNIV);
	return 0;
}

/**
 * name:	AggressiveBackupBirthdayService
 * desc:	service function for MS_USERINFO_REMINDER_AGGRASIVEBACKUP
 * param:	wParam	-	handle to single contact or NULL to backup all
 *			lParam	-	if 1, the messagebox will not be displayed
 * return:	0
 **/
static INT AggressiveBackupBirthdayService(WPARAM wParam, LPARAM lParam)
{
	if(DBGetByte(SET_REMIND_ENABLED, DEFVAL_REMIND_ENABLED)) {
		HANDLE hContact;
		MAnnivDate mdb;

		if(hContact = (HANDLE)wParam) {
			mdb.DBGetBirthDate(hContact);
			mdb.NewProtocolBirthday(hContact, NULL, TRUE);
		}
		else {
			//walk through all the contacts stored in the DB
			for(hContact = (HANDLE)CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
				hContact != NULL;
				hContact = (HANDLE)CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM)hContact, 0))
			{
				mdb.DBGetBirthDate(hContact);
				mdb.NewProtocolBirthday(hContact, NULL, TRUE);
			}
		}

		if(lParam != TRUE) {
			MSGBOX mBox;

			mBox.uSize = sizeof(MSGBOX);
			mBox.hParent = NULL;
			mBox.hiLogo = NIcoLib::GetIcon(ICO_DLG_ANNIVERSARY);
			mBox.hiMsg = NULL;
			mBox.uType = MB_ICON_INFO|MB_TCHAR;
			mBox.ptszTitle = _T("Update custom birthday");
			mBox.ptszInfoText = _T("Keeps your custom birthday up to date.");
			mBox.ptszMsg = _T("Backing up and syncing all birthdays complete!");
			MsgBoxService(NULL, (LPARAM)&mBox);
		}
	}
	return 0;
}

/**
 * name:	GetMyBirthdayModule
 * desc:	returns a constant pointer to the module the date should be saved to
 * param:	none
 * return:	module to write birthday information to, MOD_MBIRTHDAY by default
 **/
LPCSTR GetMyBirthdayModule()
{
	switch(DBGetByte(SET_REMIND_BIRTHMODULE, DEFVAL_REMIND_BIRTHMODULE)) {
		case 1:
			return USERINFO;
	}
	// this is the default
	return MOD_MBIRTHDAY;
}


/***********************************************************************************************************
 * timer stuff
 ***********************************************************************************************************/

/**
 * name:	TimerProc_DateChanged
 * desc:	Timer procedure, called if date changed. This updates clist icons.
 * param:	hwnd	- not used
 *			uMsg	- not used
 *			idEvent	- not used
 *			dwTime	- not used
 * return:	nothing
 **/
static VOID CALLBACK TimerProc_DateChanged(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) 
{
	static MTime last;
	MTime now;

	now.GetLocalTime();
	if(now.Day() > last.Day() || now.Month() > last.Month() || now.Year() > last.Year()) {
		CheckAll(NOTIFY_CLIST);
		last = now;
	}
}

/**
 * name:	TimerProc_Check
 * desc:	Timer procedure, called again and again if the notification interval ellapsed
 * param:	hwnd	- not used
 *			uMsg	- not used
 *			idEvent	- not used
 *			dwTime	- not used
 * return:	nothing
 **/
static VOID CALLBACK TimerProc_Check(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) 
{
	CheckAll(NOTIFY_POPUP);
}

/**
 * name:	UpdateTimer
 * desc:	load timers or update them
 * param:	bStartup - is only TRUE if module is loaded to indicate startup process
 * return:	nothing
 **/
static VOID UpdateTimer(BOOLEAN bStartup)
{
	LONG	wNotifyInterval =  60 * 60 * (LONG)DBGetSettingWord(NULL, MODNAME, SET_REMIND_NOTIFYINTERVAL, DEFVAL_REMIND_NOTIFYINTERVAL);
	MTime	now, last;
	
	now.GetTimeUTC();
	
	if(bStartup) {
		last.DBGetStamp(NULL, MODNAME, SET_REMIND_LASTCHECK);
		
		// if last check occured at least one day before just do it on startup again
		if(now.Year() > last.Year() ||  now.Month() > last.Month() ||  now.Day() > last.Day() || DBGetByte(SET_REMIND_CHECKON_STARTUP, FALSE))
			wNotifyInterval = 5;
		else
			wNotifyInterval -= now.Compare(last);

		ghRemindDateChangeTimer = SetTimer(0, 0, 1000 * 60 * 5, (TIMERPROC)TimerProc_DateChanged);
	}
	else {
		now.DBWriteStamp(NULL, MODNAME, SET_REMIND_LASTCHECK);
	}
	// wait at least 5 seconds before checking at startup, to give miranda a better chance to load faster
	KillTimer(0, ghRemindTimer);
	ghRemindTimer = SetTimer(0, 0, 1000 * wNotifyInterval, TimerProc_Check);
}

/***********************************************************************************************************
 * module loading & unloading
 ***********************************************************************************************************/

VOID Enable(BOOLEAN bEnable)
{
	if(bEnable) {	
		// init hooks
		if(!ghCListIR) ghCListIR = HookEvent(ME_CLIST_EXTRA_LIST_REBUILD, OnCListRebuildIcons);
		if(!ghCListIA) ghCListIA = HookEvent(ME_CLIST_EXTRA_IMAGE_APPLY, OnCListApplyIcons);
		if(!ghSettingsChanged) ghSettingsChanged = HookEvent(ME_DB_CONTACT_SETTINGCHANGED, OnContactSettingChanged);
		
		// reinit reminder options
		gRemindOpts.RemindState = DBGetSettingByte(NULL, MODNAME, SET_REMIND_ENABLED, DEFVAL_REMIND_ENABLED);
		gRemindOpts.wDaysEarlier = DBGetSettingWord(NULL, MODNAME, SET_REMIND_OFFSET, DEFVAL_REMIND_OFFSET);
		gRemindOpts.bCListExtraIcon = ColumnType(DBGetSettingByte(NULL, MODNAME, SET_REMIND_EXTRAICON, 1));
		gRemindOpts.bCheckVisibleOnly = DBGetSettingByte(NULL, MODNAME, SET_REMIND_CHECKVISIBLE, DEFVAL_REMIND_CHECKVISIBLE);
		gRemindOpts.bFlashCList = DBGetByte(SET_REMIND_FLASHICON, FALSE);
		gRemindOpts.bPopups = DBGetSettingByte(NULL, MODNAME, SET_POPUP_ENABLED, DEFVAL_POPUP_ENABLED);
#ifdef _UNICODE
		gRemindOpts.bPopups &= ServiceExists(MS_POPUP_ADDPOPUPW);
#else
		gRemindOpts.bPopups &= ServiceExists(MS_POPUP_ADDPOPUP);
#endif

		// init message handler for popups
		if(!ghWndHotkey && ServiceExists(MS_POPUP_ADDPOPUPT)) {
			WNDCLASSEX wc;
	
			ZeroMemory(&wc, sizeof(wc));
			wc.cbSize         = sizeof(wc);
			wc.hInstance	  = ghInst;
			wc.lpszClassName  = HOTKEYCLASS;
			wc.lpfnWndProc    = Hotkey_WndProc;
			wc.style          = CS_GLOBALCLASS;
			if(!RegisterClassEx(&wc) || !(ghWndHotkey = CreateWindow(HOTKEYCLASS, NULL, WS_OVERLAPPED, 0, 0, 0, 0, HWND_MESSAGE, NULL, ghInst, NULL)))
				MsgErr(NULL, _T("Can't create hotkey handler\nError %d"), GetLastError());
		}
		// init the timer
		UpdateTimer(TRUE);
	}
	else {
		UnloadModule();
	}
}

/**
 * name:	LoadModule
 * desc:	initially load all required stuff for reminder
 * param:	none
 * return:	nothing
 **/
VOID LoadModule(VOID)
{
	// init sounds
	SKINSOUNDDESCEX ssd;
	
	ZeroMemory(&ssd, sizeof(ssd));
	ssd.cbSize = sizeof(ssd);
	ssd.pszSection = MODNAME;

	ssd.pszName = SOUND_BIRTHDAY_TODAY;
	ssd.pszDescription = Translate("Birthday reminder");
	ssd.pszDefaultFile = "Sounds\\BirthDay.wav";
	CallService(MS_SKIN_ADDNEWSOUND, 0, (LPARAM)&ssd);

	ssd.pszName = SOUND_BIRTHDAY_SOON;
	ssd.pszDescription = Translate("Birthday reminder: it's coming");
	ssd.pszDefaultFile = "Sounds\\BirthDayComing.wav";
	CallService(MS_SKIN_ADDNEWSOUND, 0, (LPARAM)&ssd);

	ssd.pszName = SOUND_ANNIVERSARY;
	ssd.pszDescription = Translate("Anniversary Reminder");
	ssd.pszDefaultFile = "Sounds\\Reminder.wav";
	CallService(MS_SKIN_ADDNEWSOUND, 0, (LPARAM)&ssd);

	// init clist extra icon structure
	OnCListRebuildIcons(0, 0);

	// create service functions
	CreateServiceFunction(MS_USERINFO_REMINDER_CHECK, CheckService);
	CreateServiceFunction(MS_USERINFO_REMINDER_AGGRASIVEBACKUP, AggressiveBackupBirthdayService);

	Enable(DBGetByte(SET_REMIND_ENABLED, DEFVAL_REMIND_ENABLED) != REMIND_OFF);
}

/**
 * name:	UnloadModule
 * desc:	unload the reminder module
 * param:	none
 * return:	nothing
 **/
VOID UnloadModule(VOID)
{
	// kill timers
	KillTimer(0, ghRemindTimer);
	ghRemindTimer = 0;
	KillTimer(0, ghRemindDateChangeTimer);
	ghRemindDateChangeTimer = 0;
	
	// unhook event handlers
	UnhookEvent(ghCListIR); 
	ghCListIR = 0;
	UnhookEvent(ghCListIA); 
	ghCListIA = 0;
	UnhookEvent(ghSettingsChanged); 
	ghSettingsChanged = 0;
	
	// popup hotkey class
	DestroyWindow(ghWndHotkey);
	ghWndHotkey = NULL;
	UnregisterClass(HOTKEYCLASS, ghInst);
}

} // namespace NReminder
